Installation and loading

To install sf from CRAN:

install.packages("sf")  

or the development version from GitHub:

devtools::install_github("edzer/sfr")

then load:

library(sf)

NB MacOSX and LINUX users need to install a number of geospatial libraries (GEOS, GDAL, and proj.4).

The tidyverse package also needs to be loaded:

library(tidyverse)

Example data

A GeoJSON of Greater Manchester’s wards was created from a vector boundary file available from ONS’s Open Geography Portal. The GeoJSON is projected in British National Grid (EPSG:27700) and originally derives from the Ordnance Survey.

Point data are supplied by data.police.uk and represent incidents of anti-social behaviour and crime recorded by the Greater Manchester Police during February 2017. The incidents are supplied with latitude and longitude coordinates which have undergone an anonymisation process.

Reading and writing spatial data

Reading spatial data (polygons)

bdy <- st_read("data/wards.geojson", quiet = TRUE)

Reading data with coordinates (points)

pts <- read_csv("data/2017-03-greater-manchester-street.csv")
pts <- st_as_sf(pts, coords = c("Longitude", "Latitude"))

Writing spatial data

st_write(bdy, "boundaries.shp")

sf objects

Attribute table (dataframe) AND geometry (list-column) with coordinates, CRS, bbox

class(bdy)
[1] "sf"         "data.frame"
head(bdy)
Simple feature collection with 6 features and 12 fields
geometry type:  POLYGON
dimension:      XY
bbox:           xmin: 351664 ymin: 394521.5 xmax: 369078.9 ymax: 407481
epsg (SRID):    27700
proj4string:    +proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 +units=m +no_defs
  objectid    wd16cd      wd16nm wd16nmw   lad16cd lad16nm  bng_e  bng_n     long      lat
1      772 E05000851        Ince    <NA> E08000010   Wigan 359959 405278 -2.60570 53.54262
2      773 E05000852  Leigh East    <NA> E08000010   Wigan 367298 400695 -2.49447 53.50194
3      774 E05000853 Leigh South    <NA> E08000010   Wigan 366260 398660 -2.50990 53.48358
4      775 E05000854  Leigh West    <NA> E08000010   Wigan 363428 400762 -2.55282 53.50228
5      776 E05000855 Lowton East    <NA> E08000010   Wigan 362642 397699 -2.56431 53.47470
6      777 E05000856      Orrell    <NA> E08000010   Wigan 353321 404083 -2.70568 53.53133
  st_areasha st_lengths                       geometry
1    4780173   11442.63 POLYGON((359198.460435328 4...
2    4972092   11056.20 POLYGON((367700.613520717 4...
3    6521214   12377.97 POLYGON((364489.980933077 3...
4    7315344   16159.92 POLYGON((363107.910792193 3...
5   11185415   17990.73 POLYGON((361792.312268886 3...
6    8884317   19650.85 POLYGON((353418.76434581 40...
as.tibble(bdy)
bdy_df <- st_set_geometry(bdy, NULL) # remove geometry
head(bdy_df)
st_geometry(bdy) # print geometry
Geometry set for 215 features 
geometry type:  POLYGON
dimension:      XY
bbox:           xmin: 351664 ymin: 381168.6 xmax: 406087.5 ymax: 421039.8
epsg (SRID):    27700
proj4string:    +proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 +units=m +no_defs
First 5 geometries:

Manipulate using dplyr

Rename variables

bdy <- bdy %>% 
  select(ward = wd16nm, census_code = wd16cd, borough = lad16nm) # rename variables
glimpse(bdy)
Observations: 215
Variables: 4
$ ward        <fctr> Ince, Leigh East, Leigh South, Leigh West, Lowton East, Orrell, Pemberto...
$ census_code <fctr> E05000851, E05000852, E05000853, E05000854, E05000855, E05000856, E05000...
$ borough     <fctr> Wigan, Wigan, Wigan, Wigan, Wigan, Wigan, Wigan, Wigan, Wigan, Wigan, Wi...
$ geometry    <simple_feature> POLYGON((359198.460435328 4..., POLYGON((367700.613520717 4......

Count frequency of wards by borough

bdy %>% 
  group_by(borough) %>% 
  count() %>% 
  arrange(desc(n)) %>% # sort in descending order
  st_set_geometry(., NULL) # hide geometry

Select features

chorlton <- bdy %>% 
  filter(ward == "Chorlton")
plot(st_geometry(bdy))
plot(st_geometry(chorlton), col = "red", add = TRUE)

Using sf functions to add geometry column to dplyr chain

bdy <- bdy %>% 
  mutate(area = st_area(.)) # returns the area of a feature
bdy %>% 
  select(ward, area) %>% 
  arrange(desc(area)) %>% 
  slice(1:10) %>% 
  st_set_geometry(., NULL)

Projection

Check and assign CRS

st_crs(pts) 
$epsg
[1] NA

$proj4string
[1] NA

attr(,"class")
[1] "crs"
pts <- st_set_crs(pts, 4326) # assign Lat/Long (epsg:4326)
st_crs(pts)
$epsg
[1] 4326

$proj4string
[1] "+proj=longlat +datum=WGS84 +no_defs"

attr(,"class")
[1] "crs"

Reproject CRS

bdy_WGS84 <- st_transform(bdy, 4326)
st_crs(bdy_WGS84)
$epsg
[1] 4326

$proj4string
[1] "+proj=longlat +datum=WGS84 +no_defs"

attr(,"class")
[1] "crs"

Convert to and from sp objects

Convert to and from sp objects

bdy_sp <- as(bdy, 'Spatial')
class(bdy_sp)
[1] "SpatialPolygonsDataFrame"
attr(,"package")
[1] "sp"
bdy_sf <- st_as_sf(bdy_sp)
class(bdy_sf)
[1] "sf"         "data.frame"

Spatial operations

Buffer features

buffer <- chorlton %>% 
  st_buffer(dist = 1000)
plot(st_geometry(buffer))
plot(st_geometry(chorlton), col = "red", add = TRUE)

Buffer and intersect

pts_sub <- bdy %>%
  filter(ward == "Chorlton") %>%
  st_buffer(dist = 1000) %>%
  st_intersection(st_transform(pts, 27700)) # reproject pts to BNG (epsg:27700)
plot(st_geometry(buffer))
plot(st_geometry(chorlton), col = "red", add = TRUE)
plot(st_geometry(pts_sub), col = "black", add = TRUE)

pts_sub %>% 
  group_by(`Crime type`) %>%
  count() %>% 
  arrange(desc(n)) %>% 
  st_set_geometry(., NULL)

Points in polygon

pts %>% 
  filter(`Crime type` == "Vehicle crime") %>%  
  st_join(bdy_WGS84, ., left = FALSE) %>% 
  count(ward) %>% 
  arrange(desc(n)) %>% 
  st_set_geometry(., NULL)
bdy_pts <- pts %>% 
  filter(`Crime type` == "Vehicle crime") %>%  
  st_join(bdy_WGS84, ., left = FALSE) %>% 
  count(ward)

Plotting

Base plots

plot(bdy_pts) # plots small multiples if dataframe has several attributes

plot(bdy_pts["n"]) # select the appropriate attribute to plot a single map

library(RColorBrewer) ; library(classInt)
pal <- brewer.pal(5, "RdPu")
classes <- classIntervals(bdy_pts$n, n=5, style="pretty")$brks
plot(bdy_pts["n"], 
     col = pal[findInterval(bdy_pts$n, classes, all.inside=TRUE)], 
     main = "Vehicle crime in Greater Manchester\nMarch 2017", axes = F)
legend("bottomright", legend = paste("<", round(classes[-1])), fill = pal, cex = 0.7) 

ggplot2

devtools::install_github("tidyverse/ggplot2") # NB need development version for geom_sf()
ggplot(bdy_pts) +
  geom_sf(aes(fill = n)) +
  scale_fill_gradientn('Frequency', colours=RColorBrewer::brewer.pal(5,"RdPu"), 
                       breaks = scales::pretty_breaks(n = 5)) +
  labs(fill = "Frequency",
       title = "Vehicle crime",
       subtitle = "March 2017",
       caption = "Contains OS data © Crown copyright and database right (2017)") +
  theme_void()

plotly

library(plotly)
p <- ggplot(bdy_pts) +
  geom_sf(aes(fill = n, text = paste0(ward, "\n", "Crimes: ", n))) +
  scale_fill_gradientn('Frequency', colours=RColorBrewer::brewer.pal(5,"RdPu"), 
                       breaks = scales::pretty_breaks(n = 5)) +
  labs(fill = "Frequency",
       title = "Vehicle crime",
       subtitle = "March 2017",
       caption = "Contains OS data © Crown copyright and database right (2017)") +
  theme_void() + 
  coord_fixed(1.3)
ggplotly(p, tooltip = "text")

leaflet

library(leaflet)
pal <- colorBin("RdPu", domain = bdy_pts$n, bins = 5, pretty = TRUE)
leaflet(bdy_pts) %>% 
  addTiles(urlTemplate = "http://{s}.tiles.wmflabs.org/bw-mapnik/{z}/{x}/{y}.png",
    attribution = '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>, <a href="https://www.ons.gov.uk/methodology/geography/licences">Contains OS data © Crown copyright and database right (2017)</a>') %>% 
  addPolygons(fillColor = ~pal(n), fillOpacity = 0.8,
              weight = 1, opacity = 1, color = "black",
              label = ~as.character(ward)) %>% 
  addLegend(pal = pal, values = ~n, opacity = 0.7, 
            title = 'Vehicle crime (March 2017)', position = "bottomleft") %>%
  addMiniMap(tiles = providers$CartoDB.Positron, toggleDisplay = TRUE)
LS0tCnRpdGxlOiAiU3BhdGlhbCBkYXRhIGluIFI6IGFuIGludHJvZHVjdGlvbiB0byB0aGUgc2YgcGFja2FnZSIKYXV0aG9yOiAiSGVucnkgUGFydHJpZGdlIgpkYXRlOiAiMjAxNy0wNi0wNSIKb3V0cHV0OiAKICBodG1sX25vdGVib29rOgogICAgZmlnX2NhcHRpb246IG5vCiAgICB0b2M6IHllcwogICAgdG9jX2RlcHRoOiAzCi0tLQoKYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0V9CiMjIEdsb2JhbCBjb2RlIG9wdGlvbnMKa25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG89VFJVRSwKCSAgICAgICAgICAgICBjYWNoZT1UUlVFLAogICAgICAgICAgICAgICBwcm9tcHQ9RkFMU0UsCiAgICAgICAgICAgICAgIHRpZHk9VFJVRSwKICAgICAgICAgICAgICAgY29tbWVudD1OQSwKICAgICAgICAgICAgICAgbWVzc2FnZT1GQUxTRSwKICAgICAgICAgICAgICAgd2FybmluZz1GQUxTRSkKYGBgCgoqKioKCiMjIyBJbnN0YWxsYXRpb24gYW5kIGxvYWRpbmcKClRvIGluc3RhbGwgW3NmXShodHRwczovL2NyYW4ucnN0dWRpby5jb20vd2ViL3BhY2thZ2VzL3NmLykgZnJvbSBDUkFOOgpgYGB7ciBldmFsPUZBTFNFfQppbnN0YWxsLnBhY2thZ2VzKCJzZiIpICAKYGBgCgpvciB0aGUgZGV2ZWxvcG1lbnQgdmVyc2lvbiBmcm9tIEdpdEh1YjoKYGBge3IgZXZhbD1GQUxTRX0KZGV2dG9vbHM6Omluc3RhbGxfZ2l0aHViKCJlZHplci9zZnIiKQpgYGAKCnRoZW4gbG9hZDoKYGBge3J9CmxpYnJhcnkoc2YpCmBgYAoKKipOQioqIE1hY09TWCBhbmQgTElOVVggdXNlcnMgbmVlZCB0byBpbnN0YWxsIGEgbnVtYmVyIG9mIGdlb3NwYXRpYWwgbGlicmFyaWVzIChHRU9TLCBHREFMLCBhbmQgcHJvai40KS4gICAgIAoKVGhlIFt0aWR5dmVyc2VdKGh0dHBzOi8vY3Jhbi5yLXByb2plY3Qub3JnL3dlYi9wYWNrYWdlcy90aWR5dmVyc2UvaW5kZXguaHRtbCkgcGFja2FnZSBhbHNvIG5lZWRzIHRvIGJlIGxvYWRlZDoKYGBge3J9CmxpYnJhcnkodGlkeXZlcnNlKQpgYGAKCiMjIyBFeGFtcGxlIGRhdGEgICAgIApBIEdlb0pTT04gb2YgR3JlYXRlciBNYW5jaGVzdGVyJ3Mgd2FyZHMgd2FzIGNyZWF0ZWQgZnJvbSBhIHZlY3RvciBib3VuZGFyeSBmaWxlIGF2YWlsYWJsZSBmcm9tIFtPTlMncyBPcGVuIEdlb2dyYXBoeSBQb3J0YWxdKGh0dHA6Ly9nZW9wb3J0YWwuc3RhdGlzdGljcy5nb3YudWsvZGF0YXNldHMvYWZjYzg4YWZmZTVmNDUwZTljMDM5NzBiMjM3YTc5OTlfMikuIFRoZSBHZW9KU09OIGlzIHByb2plY3RlZCBpbiBCcml0aXNoIE5hdGlvbmFsIEdyaWQgKEVQU0c6Mjc3MDApIGFuZCBvcmlnaW5hbGx5IGRlcml2ZXMgZnJvbSB0aGUgW09yZG5hbmNlIFN1cnZleV0oaHR0cHM6Ly93d3cub3JkbmFuY2VzdXJ2ZXkuY28udWsvYnVzaW5lc3MtYW5kLWdvdmVybm1lbnQvcHJvZHVjdHMvb3BlbmRhdGEtcHJvZHVjdHMuaHRtbCkuCgpQb2ludCBkYXRhIGFyZSBzdXBwbGllZCBieSBbZGF0YS5wb2xpY2UudWtdKGh0dHA6Ly9kYXRhLnBvbGljZS51aykgYW5kIHJlcHJlc2VudCBpbmNpZGVudHMgb2YgYW50aS1zb2NpYWwgYmVoYXZpb3VyIGFuZCBjcmltZSByZWNvcmRlZCBieSB0aGUgR3JlYXRlciBNYW5jaGVzdGVyIFBvbGljZSBkdXJpbmcgRmVicnVhcnkgMjAxNy4gVGhlIGluY2lkZW50cyBhcmUgc3VwcGxpZWQgd2l0aCBsYXRpdHVkZSBhbmQgbG9uZ2l0dWRlIGNvb3JkaW5hdGVzIHdoaWNoIGhhdmUgdW5kZXJnb25lIGFuIFthbm9ueW1pc2F0aW9uIHByb2Nlc3NdKGh0dHA6Ly9kYXRhLnBvbGljZS51ay9hYm91dC8jYW5vbnltaXNhdGlvbikuCgojIyMgUmVhZGluZyBhbmQgd3JpdGluZyBzcGF0aWFsIGRhdGEKClJlYWRpbmcgc3BhdGlhbCBkYXRhIChwb2x5Z29ucykKYGBge3J9CmJkeSA8LSBzdF9yZWFkKCJkYXRhL3dhcmRzLmdlb2pzb24iLCBxdWlldCA9IFRSVUUpCmBgYAoKUmVhZGluZyBkYXRhIHdpdGggY29vcmRpbmF0ZXMgKHBvaW50cykKYGBge3J9CnB0cyA8LSByZWFkX2NzdigiZGF0YS8yMDE3LTAzLWdyZWF0ZXItbWFuY2hlc3Rlci1zdHJlZXQuY3N2IikKcHRzIDwtIHN0X2FzX3NmKHB0cywgY29vcmRzID0gYygiTG9uZ2l0dWRlIiwgIkxhdGl0dWRlIikpCmBgYAoKV3JpdGluZyBzcGF0aWFsIGRhdGEKYGBge3IgZXZhbD1GQUxTRX0Kc3Rfd3JpdGUoYmR5LCAiYm91bmRhcmllcy5zaHAiKQpgYGAKCiMjIyBzZiBvYmplY3RzCgpBdHRyaWJ1dGUgdGFibGUgKGRhdGFmcmFtZSkgQU5EIGdlb21ldHJ5IChsaXN0LWNvbHVtbikgd2l0aCBjb29yZGluYXRlcywgQ1JTLCBiYm94CmBgYHtyfQpjbGFzcyhiZHkpCmhlYWQoYmR5KQphcy50aWJibGUoYmR5KQpiZHlfZGYgPC0gc3Rfc2V0X2dlb21ldHJ5KGJkeSwgTlVMTCkgIyByZW1vdmUgZ2VvbWV0cnkKaGVhZChiZHlfZGYpCnN0X2dlb21ldHJ5KGJkeSkgIyBwcmludCBnZW9tZXRyeQpgYGAKCiMjIyBNYW5pcHVsYXRlIHVzaW5nIGRwbHlyCgpSZW5hbWUgdmFyaWFibGVzCmBgYHtyfQpiZHkgPC0gYmR5ICU+JSAKICBzZWxlY3Qod2FyZCA9IHdkMTZubSwgY2Vuc3VzX2NvZGUgPSB3ZDE2Y2QsIGJvcm91Z2ggPSBsYWQxNm5tKSAjIHJlbmFtZSB2YXJpYWJsZXMKZ2xpbXBzZShiZHkpCmBgYAoKQ291bnQgZnJlcXVlbmN5IG9mIHdhcmRzIGJ5IGJvcm91Z2gKYGBge3J9CmJkeSAlPiUgCiAgZ3JvdXBfYnkoYm9yb3VnaCkgJT4lIAogIGNvdW50KCkgJT4lIAogIGFycmFuZ2UoZGVzYyhuKSkgJT4lICMgc29ydCBpbiBkZXNjZW5kaW5nIG9yZGVyCiAgc3Rfc2V0X2dlb21ldHJ5KC4sIE5VTEwpICMgaGlkZSBnZW9tZXRyeQpgYGAKClNlbGVjdCBmZWF0dXJlcwpgYGB7cn0KY2hvcmx0b24gPC0gYmR5ICU+JSAKICBmaWx0ZXIod2FyZCA9PSAiQ2hvcmx0b24iKQpwbG90KHN0X2dlb21ldHJ5KGJkeSkpCnBsb3Qoc3RfZ2VvbWV0cnkoY2hvcmx0b24pLCBjb2wgPSAicmVkIiwgYWRkID0gVFJVRSkKYGBgCgpVc2luZyBzZiBmdW5jdGlvbnMgdG8gYWRkIGdlb21ldHJ5IGNvbHVtbiB0byBkcGx5ciBjaGFpbgpgYGB7cn0KYmR5IDwtIGJkeSAlPiUgCiAgbXV0YXRlKGFyZWEgPSBzdF9hcmVhKC4pKSAjIHJldHVybnMgdGhlIGFyZWEgb2YgYSBmZWF0dXJlCgpiZHkgJT4lIAogIHNlbGVjdCh3YXJkLCBhcmVhKSAlPiUgCiAgYXJyYW5nZShkZXNjKGFyZWEpKSAlPiUgCiAgc2xpY2UoMToxMCkgJT4lIAogIHN0X3NldF9nZW9tZXRyeSguLCBOVUxMKQpgYGAKCiMjIyBQcm9qZWN0aW9uCgpDaGVjayBhbmQgYXNzaWduIENSUwpgYGB7cn0Kc3RfY3JzKHB0cykgCnB0cyA8LSBzdF9zZXRfY3JzKHB0cywgNDMyNikgIyBhc3NpZ24gTGF0L0xvbmcgKGVwc2c6NDMyNikKc3RfY3JzKHB0cykKYGBgCgpSZXByb2plY3QgQ1JTCmBgYHtyfQpiZHlfV0dTODQgPC0gc3RfdHJhbnNmb3JtKGJkeSwgNDMyNikKc3RfY3JzKGJkeV9XR1M4NCkKYGBgCgojIyMgQ29udmVydCB0byBhbmQgZnJvbSBzcCBvYmplY3RzCgpDb252ZXJ0IHRvIGFuZCBmcm9tIFtzcF0oaHR0cHM6Ly9jcmFuLnItcHJvamVjdC5vcmcvd2ViL3BhY2thZ2VzL3NwL2luZGV4Lmh0bWwpIG9iamVjdHMKYGBge3J9CmJkeV9zcCA8LSBhcyhiZHksICdTcGF0aWFsJykKY2xhc3MoYmR5X3NwKQpgYGAKCmBgYHtyfQpiZHlfc2YgPC0gc3RfYXNfc2YoYmR5X3NwKQpjbGFzcyhiZHlfc2YpCmBgYAoKIyMjIFNwYXRpYWwgb3BlcmF0aW9ucwoKQnVmZmVyIGZlYXR1cmVzCmBgYHtyfQpidWZmZXIgPC0gY2hvcmx0b24gJT4lIAogIHN0X2J1ZmZlcihkaXN0ID0gMTAwMCkKcGxvdChzdF9nZW9tZXRyeShidWZmZXIpKQpwbG90KHN0X2dlb21ldHJ5KGNob3JsdG9uKSwgY29sID0gInJlZCIsIGFkZCA9IFRSVUUpCmBgYAoKQnVmZmVyIGFuZCBpbnRlcnNlY3QKYGBge3J9CnB0c19zdWIgPC0gYmR5ICU+JQogIGZpbHRlcih3YXJkID09ICJDaG9ybHRvbiIpICU+JQogIHN0X2J1ZmZlcihkaXN0ID0gMTAwMCkgJT4lCiAgc3RfaW50ZXJzZWN0aW9uKHN0X3RyYW5zZm9ybShwdHMsIDI3NzAwKSkgIyByZXByb2plY3QgcHRzIHRvIEJORyAoZXBzZzoyNzcwMCkKCnBsb3Qoc3RfZ2VvbWV0cnkoYnVmZmVyKSkKcGxvdChzdF9nZW9tZXRyeShjaG9ybHRvbiksIGNvbCA9ICJyZWQiLCBhZGQgPSBUUlVFKQpwbG90KHN0X2dlb21ldHJ5KHB0c19zdWIpLCBjb2wgPSAiYmxhY2siLCBhZGQgPSBUUlVFKQpgYGAKCmBgYHtyfQpwdHNfc3ViICU+JSAKICBncm91cF9ieShgQ3JpbWUgdHlwZWApICU+JQogIGNvdW50KCkgJT4lIAogIGFycmFuZ2UoZGVzYyhuKSkgJT4lIAogIHN0X3NldF9nZW9tZXRyeSguLCBOVUxMKQpgYGAKClBvaW50cyBpbiBwb2x5Z29uCmBgYHtyfQpwdHMgJT4lIAogIGZpbHRlcihgQ3JpbWUgdHlwZWAgPT0gIlZlaGljbGUgY3JpbWUiKSAlPiUgIAogIHN0X2pvaW4oYmR5X1dHUzg0LCAuLCBsZWZ0ID0gRkFMU0UpICU+JSAKICBjb3VudCh3YXJkKSAlPiUgCiAgYXJyYW5nZShkZXNjKG4pKSAlPiUgCiAgc3Rfc2V0X2dlb21ldHJ5KC4sIE5VTEwpCgpiZHlfcHRzIDwtIHB0cyAlPiUgCiAgZmlsdGVyKGBDcmltZSB0eXBlYCA9PSAiVmVoaWNsZSBjcmltZSIpICU+JSAgCiAgc3Rfam9pbihiZHlfV0dTODQsIC4sIGxlZnQgPSBGQUxTRSkgJT4lIAogIGNvdW50KHdhcmQpCmBgYAoKIyMjIFBsb3R0aW5nCgpCYXNlIHBsb3RzCmBgYHtyfQpwbG90KGJkeV9wdHMpICMgcGxvdHMgc21hbGwgbXVsdGlwbGVzIGlmIGRhdGFmcmFtZSBoYXMgc2V2ZXJhbCBhdHRyaWJ1dGVzCmBgYAoKYGBge3J9CnBsb3QoYmR5X3B0c1sibiJdKSAjIHNlbGVjdCB0aGUgYXBwcm9wcmlhdGUgYXR0cmlidXRlIHRvIHBsb3QgYSBzaW5nbGUgbWFwCmBgYAoKYGBge3J9CmxpYnJhcnkoUkNvbG9yQnJld2VyKSA7IGxpYnJhcnkoY2xhc3NJbnQpCnBhbCA8LSBicmV3ZXIucGFsKDUsICJSZFB1IikKY2xhc3NlcyA8LSBjbGFzc0ludGVydmFscyhiZHlfcHRzJG4sIG49NSwgc3R5bGU9InByZXR0eSIpJGJya3MKcGxvdChiZHlfcHRzWyJuIl0sIAogICAgIGNvbCA9IHBhbFtmaW5kSW50ZXJ2YWwoYmR5X3B0cyRuLCBjbGFzc2VzLCBhbGwuaW5zaWRlPVRSVUUpXSwgCiAgICAgbWFpbiA9ICJWZWhpY2xlIGNyaW1lIGluIEdyZWF0ZXIgTWFuY2hlc3RlclxuTWFyY2ggMjAxNyIsIGF4ZXMgPSBGKQpsZWdlbmQoImJvdHRvbXJpZ2h0IiwgbGVnZW5kID0gcGFzdGUoIjwiLCByb3VuZChjbGFzc2VzWy0xXSkpLCBmaWxsID0gcGFsLCBjZXggPSAwLjcpIApgYGAKCltnZ3Bsb3QyXShodHRwczovL2NyYW4uci1wcm9qZWN0Lm9yZy93ZWIvcGFja2FnZXMvZ2dwbG90Mi9pbmRleC5odG1sKQpgYGB7ciwgZXZhbD1GQUxTRX0KZGV2dG9vbHM6Omluc3RhbGxfZ2l0aHViKCJ0aWR5dmVyc2UvZ2dwbG90MiIpICMgTkIgbmVlZCBkZXZlbG9wbWVudCB2ZXJzaW9uIGZvciBnZW9tX3NmKCkKYGBgCgpgYGB7cn0KZ2dwbG90KGJkeV9wdHMpICsKICBnZW9tX3NmKGFlcyhmaWxsID0gbikpICsKICBzY2FsZV9maWxsX2dyYWRpZW50bignRnJlcXVlbmN5JywgY29sb3Vycz1SQ29sb3JCcmV3ZXI6OmJyZXdlci5wYWwoNSwiUmRQdSIpLCAKICAgICAgICAgICAgICAgICAgICAgICBicmVha3MgPSBzY2FsZXM6OnByZXR0eV9icmVha3MobiA9IDUpKSArCiAgbGFicyhmaWxsID0gIkZyZXF1ZW5jeSIsCiAgICAgICB0aXRsZSA9ICJWZWhpY2xlIGNyaW1lIiwKICAgICAgIHN1YnRpdGxlID0gIk1hcmNoIDIwMTciLAogICAgICAgY2FwdGlvbiA9ICJDb250YWlucyBPUyBkYXRhIMKpIENyb3duIGNvcHlyaWdodCBhbmQgZGF0YWJhc2UgcmlnaHQgKDIwMTcpIikgKwogIHRoZW1lX3ZvaWQoKQpgYGAKCltwbG90bHldKGh0dHBzOi8vY3Jhbi5yLXByb2plY3Qub3JnL3dlYi9wYWNrYWdlcy9wbG90bHkvaW5kZXguaHRtbCkKYGBge3J9CmxpYnJhcnkocGxvdGx5KQpwIDwtIGdncGxvdChiZHlfcHRzKSArCiAgZ2VvbV9zZihhZXMoZmlsbCA9IG4sIHRleHQgPSBwYXN0ZTAod2FyZCwgIlxuIiwgIkNyaW1lczogIiwgbikpKSArCiAgc2NhbGVfZmlsbF9ncmFkaWVudG4oJ0ZyZXF1ZW5jeScsIGNvbG91cnM9UkNvbG9yQnJld2VyOjpicmV3ZXIucGFsKDUsIlJkUHUiKSwgCiAgICAgICAgICAgICAgICAgICAgICAgYnJlYWtzID0gc2NhbGVzOjpwcmV0dHlfYnJlYWtzKG4gPSA1KSkgKwogIGxhYnMoZmlsbCA9ICJGcmVxdWVuY3kiLAogICAgICAgdGl0bGUgPSAiVmVoaWNsZSBjcmltZSIsCiAgICAgICBzdWJ0aXRsZSA9ICJNYXJjaCAyMDE3IiwKICAgICAgIGNhcHRpb24gPSAiQ29udGFpbnMgT1MgZGF0YSDCqSBDcm93biBjb3B5cmlnaHQgYW5kIGRhdGFiYXNlIHJpZ2h0ICgyMDE3KSIpICsKICB0aGVtZV92b2lkKCkgKyAKICBjb29yZF9maXhlZCgxLjMpCmdncGxvdGx5KHAsIHRvb2x0aXAgPSAidGV4dCIpCmBgYAoKW2xlYWZsZXRdKGh0dHBzOi8vY3Jhbi5yLXByb2plY3Qub3JnL3dlYi9wYWNrYWdlcy9sZWFmbGV0L2luZGV4Lmh0bWwpCmBgYHtyfQpsaWJyYXJ5KGxlYWZsZXQpCnBhbCA8LSBjb2xvckJpbigiUmRQdSIsIGRvbWFpbiA9IGJkeV9wdHMkbiwgYmlucyA9IDUsIHByZXR0eSA9IFRSVUUpCmxlYWZsZXQoYmR5X3B0cykgJT4lIAogIGFkZFRpbGVzKHVybFRlbXBsYXRlID0gImh0dHA6Ly97c30udGlsZXMud21mbGFicy5vcmcvYnctbWFwbmlrL3t6fS97eH0ve3l9LnBuZyIsCiAgICBhdHRyaWJ1dGlvbiA9ICcmY29weTsgPGEgaHJlZj0iaHR0cDovL3d3dy5vcGVuc3RyZWV0bWFwLm9yZy9jb3B5cmlnaHQiPk9wZW5TdHJlZXRNYXA8L2E+LCA8YSBocmVmPSJodHRwczovL3d3dy5vbnMuZ292LnVrL21ldGhvZG9sb2d5L2dlb2dyYXBoeS9saWNlbmNlcyI+Q29udGFpbnMgT1MgZGF0YSDCqSBDcm93biBjb3B5cmlnaHQgYW5kIGRhdGFiYXNlIHJpZ2h0ICgyMDE3KTwvYT4nKSAlPiUgCiAgYWRkUG9seWdvbnMoZmlsbENvbG9yID0gfnBhbChuKSwgZmlsbE9wYWNpdHkgPSAwLjgsCiAgICAgICAgICAgICAgd2VpZ2h0ID0gMSwgb3BhY2l0eSA9IDEsIGNvbG9yID0gImJsYWNrIiwKICAgICAgICAgICAgICBsYWJlbCA9IH5hcy5jaGFyYWN0ZXIod2FyZCkpICU+JSAKICBhZGRMZWdlbmQocGFsID0gcGFsLCB2YWx1ZXMgPSB+biwgb3BhY2l0eSA9IDAuNywgCiAgICAgICAgICAgIHRpdGxlID0gJ1ZlaGljbGUgY3JpbWUgKE1hcmNoIDIwMTcpJywgcG9zaXRpb24gPSAiYm90dG9tbGVmdCIpICU+JQogIGFkZE1pbmlNYXAodGlsZXMgPSBwcm92aWRlcnMkQ2FydG9EQi5Qb3NpdHJvbiwgdG9nZ2xlRGlzcGxheSA9IFRSVUUpCmBgYA==